FAB ACADEMY | XIAO ESP32-C3 | ESP32 | MQTT · MOSQUITTO · WIFI
This week builds directly on the interface week. In Week 14 I had the XIAO ESP32-C3 reading the LSM6DS33 IMU over I2C and streaming data over USB serial to a Python backend. MQTT was part of the plan but I skipped it to focus on WebSocket and the dashboard first. This week I completed that piece and took it further — both boards now communicate wirelessly over WiFi through a local MQTT broker, with no USB cable involved.
The XIAO ESP32-C3 connects to WiFi and publishes live accelerometer and gyroscope readings to a topic on the Mosquitto broker running on my laptop. A second board — a standard ESP32 — subscribes to that same topic and watches the accelerometer values. When the sensor is shaken hard enough that the acceleration exceeds a threshold, the ESP32 turns on an LED. When it drops back below the threshold the LED turns off. Two completely separate boards, communicating wirelessly, with no direct connection between them.
| Board | Role | Topic |
|---|---|---|
| XIAO ESP32-C3 + LSM6DS33 | Publisher publishes sensor readings | fab/imu |
| ESP32 | Subscriber receives readings, controls LED | fab/imu |
| Laptop (Mosquitto) | Broker routes messages between boards | all topics |
MQTT (Message Queuing Telemetry Transport) is a lightweight messaging protocol
designed for devices that need to communicate over a network without talking to
each other directly. Instead of Device A sending data straight to Device B, both
connect to a central broker. Device A publishes
a message to a topic — which is just a string like
fab/imu — and Device B subscribes to that topic.
The broker receives the published message and forwards it to every subscriber.
The publisher and subscriber never need to know each other's IP address or even
that the other exists.
This makes it very well suited to hardware projects — you can add more subscribers (a dashboard, a data logger, a second actuator) without changing anything on the publisher side. The broker handles all the routing.
// KEY POINT Both boards connect to the broker's IP address (the laptop's IP on the local WiFi network). They don't connect to each other. The broker is the only thing they both need to know about.
Mosquitto is a lightweight open-source MQTT broker. I already had it installed from the interface week. To allow the ESP32 boards to connect over WiFi (rather than just localhost), Mosquitto needs a config file that allows connections from outside the laptop. By default it only accepts connections from the same machine.
I created a file called mosquitto.conf with the following:
listener 1883 allow_anonymous true
Then started the broker with:
mosquitto -c mosquitto.conf
To find the laptop's IP address on the local network (which the boards need to
connect to) I ran ipconfig in the terminal and looked for the
IPv4 address under the WiFi adapter — something like 192.168.x.x.
Both boards use this address as the broker IP in their firmware.
// NOTE The laptop and both ESP32 boards must all be on the same WiFi network. If the boards connect to a different network than the laptop, they cannot reach the broker.
The XIAO ESP32-C3 already had the LSM6DS33 firmware from Week 14 which read the sensor and sent CSV over serial. For this week I rewrote the firmware to connect to WiFi and publish the readings directly to the MQTT broker over the network — no USB serial or Python backend needed. It uses the PubSubClient Arduino library for MQTT and the built-in WiFi library for the network connection.
On startup the board connects to WiFi and then connects to the broker at the
laptop's IP address on port 1883. In the loop it reads the sensor, formats the
6 values as a JSON string and publishes it to fab/imu every 100ms.
If the broker connection drops it automatically tries to reconnect.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_LSM6DS33.h>
const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
const char* broker = "192.168.x.x"; // laptop IP
WiFiClient wifi;
PubSubClient mqtt(wifi);
Adafruit_LSM6DS33 lsm;
void connectWifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
}
void connectMQTT() {
while (!mqtt.connected()) {
mqtt.connect("xiao-publisher");
delay(500);
}
}
void setup() {
Serial.begin(115200);
connectWifi();
mqtt.setServer(broker, 1883);
connectMQTT();
lsm.begin_I2C();
lsm.setAccelRange(LSM6DS_ACCEL_RANGE_8_G);
lsm.setGyroRange(LSM6DS_GYRO_RANGE_500_DPS);
}
void loop() {
if (!mqtt.connected()) connectMQTT();
mqtt.loop();
sensors_event_t accel, gyro, temp;
lsm.getEvent(&accel, &gyro, &temp);
char payload[128];
snprintf(payload, sizeof(payload),
"{\"ax\":%.2f,\"ay\":%.2f,\"az\":%.2f,\"gx\":%.2f,\"gy\":%.2f,\"gz\":%.2f}",
accel.acceleration.x, accel.acceleration.y, accel.acceleration.z,
gyro.gyro.x, gyro.gyro.y, gyro.gyro.z);
mqtt.publish("fab/imu", payload);
delay(100);
}
The ESP32 connects to the same WiFi network and subscribes to fab/imu.
Every time a message arrives the onMessage callback fires. It parses
the JSON payload, reads the three accelerometer values and calculates the total
acceleration magnitude using:
magnitude = sqrt(ax*ax + ay*ay + az*az)
At rest the magnitude sits around 9.8 (gravity). When the sensor is shaken the magnitude spikes well above that. I set the threshold at 15 m/s² — anything above that turns the LED on, anything below turns it off. The threshold was tuned by watching the serial monitor while shaking the sensor at different intensities.
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
const char* broker = "192.168.x.x";
const int LED_PIN = 2;
const float THRESHOLD = 15.0;
WiFiClient wifi;
PubSubClient mqtt(wifi);
void onMessage(char* topic, byte* payload, unsigned int length) {
StaticJsonDocument<200> doc;
deserializeJson(doc, payload, length);
float ax = doc["ax"];
float ay = doc["ay"];
float az = doc["az"];
float magnitude = sqrt(ax*ax + ay*ay + az*az);
if (magnitude > THRESHOLD) {
digitalWrite(LED_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
}
}
void connectWifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
}
void connectMQTT() {
while (!mqtt.connected()) {
if (mqtt.connect("esp32-subscriber")) {
mqtt.subscribe("fab/imu");
}
delay(500);
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
connectWifi();
mqtt.setServer(broker, 1883);
mqtt.setCallback(onMessage);
connectMQTT();
}
void loop() {
if (!mqtt.connected()) connectMQTT();
mqtt.loop();
}
// LIBRARY NEEDED The subscriber uses ArduinoJson to parse the JSON payload. Install it from Arduino Library Manager — search "ArduinoJson" by Benoit Blanchon.
With both boards flashed and the broker running, the system worked as follows — the XIAO publishes sensor data continuously, the ESP32 receives it and the LED stays off at rest. Shaking the XIAO causes the LED on the ESP32 to turn on immediately, and it turns off again once the motion stops.